﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Net;
using Microsoft.Deployment.DotNet.Releases;
using Microsoft.DotNet.Cli.Installer.Windows;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.NET.Sdk.WorkloadManifestReader;
using Microsoft.Win32;
using Microsoft.Win32.Msi;
using NuGet.Packaging.Core;
using NuGet.Versioning;

namespace Microsoft.DotNet.Cli.Commands.Workload.Install;

internal partial class NetSdkMsiInstallerClient
{
    protected List<WorkloadSetRecord> GetWorkloadSetRecords()
    {
        Log?.LogMessage($"Detecting installed workload sets for {HostArchitecture}.");

        var workloadSetRecords = new List<WorkloadSetRecord>();

        using RegistryKey installedManifestsKey = Registry.LocalMachine.OpenSubKey(@$"SOFTWARE\Microsoft\dotnet\InstalledWorkloadSets\{HostArchitecture}");
        if (installedManifestsKey != null)
        {
            foreach (string workloadSetFeatureBand in installedManifestsKey.GetSubKeyNames())
            {
                using RegistryKey workloadSetFeatureBandKey = installedManifestsKey.OpenSubKey(workloadSetFeatureBand);
                foreach (string workloadSetPackageVersion in workloadSetFeatureBandKey.GetSubKeyNames())
                {
                    using RegistryKey workloadSetPackageVersionKey = workloadSetFeatureBandKey.OpenSubKey(workloadSetPackageVersion);

                    string workloadSetVersion = WorkloadSetVersion.FromWorkloadSetPackageVersion(new SdkFeatureBand(workloadSetFeatureBand), workloadSetPackageVersion);

                    WorkloadSetRecord record = new WorkloadSetRecord()
                    {
                        ProviderKeyName = (string)workloadSetPackageVersionKey.GetValue("DependencyProviderKey"),
                        WorkloadSetVersion = workloadSetVersion,
                        WorkloadSetPackageVersion = workloadSetPackageVersion,
                        WorkloadSetFeatureBand = workloadSetFeatureBand,
                        ProductCode = (string)workloadSetPackageVersionKey.GetValue("ProductCode"),
                        ProductVersion = new Version((string)workloadSetPackageVersionKey.GetValue("ProductVersion")),
                        UpgradeCode = (string)workloadSetPackageVersionKey.GetValue("UpgradeCode"),
                    };

                    Log.LogMessage($"Found workload set record, version: {workloadSetVersion}, feature band: {workloadSetFeatureBand}, ProductCode: {record.ProductCode}, provider key: {record.ProviderKeyName}");
                    workloadSetRecords.Add(record);
                }
            }
        }

        return workloadSetRecords;
    }

    //  Manifest IDs are lowercased on disk, we need to map them back to the original casing to generate the right UpgradeCode
    private static readonly string[] CasedManifestIds =
        [
            "Microsoft.NET.Sdk.Android",
            "Microsoft.NET.Sdk.Aspire",
            "Microsoft.NET.Sdk.iOS",
            "Microsoft.NET.Sdk.MacCatalyst",
            "Microsoft.NET.Sdk.macOS",
            "Microsoft.NET.Sdk.Maui",
            "Microsoft.NET.Sdk.tvOS",
            "Microsoft.NET.Workload.Emscripten.Current",
            "Microsoft.NET.Workload.Emscripten.net6",
            "Microsoft.NET.Workload.Emscripten.net7",
            "Microsoft.NET.Workload.Emscripten.net8",
            "Microsoft.NET.Workload.Emscripten.net9",
            "Microsoft.NET.Workload.Mono.ToolChain.Current",
            "Microsoft.NET.Workload.Mono.ToolChain.net6",
            "Microsoft.NET.Workload.Mono.ToolChain.net7",
            "Microsoft.NET.Workload.Mono.ToolChain.net8",
            "Microsoft.NET.Workload.Mono.ToolChain.net9",
        ];

    private static readonly IReadOnlyDictionary<string, string> ManifestIdCasing = CasedManifestIds.ToDictionary(id => id.ToLowerInvariant()).AsReadOnly();

    protected List<WorkloadManifestRecord> GetWorkloadManifestRecords()
    {
        Log?.LogMessage($"Detecting installed workload manifests for {HostArchitecture}.");

        var manifestRecords = new List<WorkloadManifestRecord>();
        HashSet<(string id, string version)> discoveredManifests = [];

        using RegistryKey installedManifestsKey = Registry.LocalMachine.OpenSubKey(@$"SOFTWARE\Microsoft\dotnet\InstalledManifests\{HostArchitecture}");
        if (installedManifestsKey != null)
        {
            foreach (string manifestPackageId in installedManifestsKey.GetSubKeyNames())
            {
                const string ManifestSeparator = ".Manifest-";

                int separatorIndex = manifestPackageId.IndexOf(ManifestSeparator);
                if (separatorIndex < 0 || manifestPackageId.Length < separatorIndex + ManifestSeparator.Length + 1)
                {
                    Log.LogMessage($"Found apparent manifest package ID '{manifestPackageId} which did not correctly parse into manifest ID and feature band.");
                    continue;
                }

                string manifestId = manifestPackageId.Substring(0, separatorIndex);
                string manifestFeatureBand = manifestPackageId.Substring(separatorIndex + ManifestSeparator.Length);

                using RegistryKey manifestKey = installedManifestsKey.OpenSubKey(manifestPackageId);
                foreach (string manifestVersion in manifestKey.GetSubKeyNames())
                {
                    using RegistryKey manifestVersionKey = manifestKey.OpenSubKey(manifestVersion);

                    WorkloadManifestRecord record = new WorkloadManifestRecord
                    {
                        ManifestId = manifestId,
                        ManifestVersion = manifestVersion,
                        ManifestFeatureBand = manifestFeatureBand,
                        ProductCode = (string)manifestVersionKey.GetValue("ProductCode"),
                        UpgradeCode = (string)manifestVersionKey.GetValue("UpgradeCode"),
                        ProductVersion = new Version((string)manifestVersionKey.GetValue("ProductVersion")),
                        ProviderKeyName = (string)manifestVersionKey.GetValue("DependencyProviderKey")
                    };

                    Log.LogMessage($"Found workload manifest record, Id: {manifestId}, version: {manifestVersion}, feature band: {manifestFeatureBand}, ProductCode: {record.ProductCode}, provider key: {record.ProviderKeyName}");
                    manifestRecords.Add(record);
                    discoveredManifests.Add((manifestId, manifestVersion));
                }
            }
        }

        //  Workload manifest MSIs for 8.0.100 don't yet write the same type of installation records to the registry that workload packs do.
        //  So to find what is installed, we look for the manifests on disk, and then map that to installed MSIs
        //  To do the mapping to installed MSIs, we rely on the fact that the MSI UpgradeCode is generated in a stable fashion from the
        //  NuGet package identity and platform: Utils.CreateUuid(UpgradeCodeNamespaceUuid, $"{Package.Identity};{Platform}")
        //  The NuGet package identity used is the vanilla (non-MSI) manifest package, for example Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-8.0.100 version 8.0.0
        string sdkManifestFolder = Path.Combine(DotNetHome, "sdk-manifests");

        foreach (var manifestFeatureBandFolder in Directory.GetDirectories(sdkManifestFolder))
        {
            if (!ReleaseVersion.TryParse(Path.GetFileName(manifestFeatureBandFolder), out ReleaseVersion releaseVersion))
            {
                //  Ignore folders which aren't valid version numbers
                Log.LogMessage($"Skipping invalid feature band version folder: {manifestFeatureBandFolder}");
                continue;
            }
            if (releaseVersion.Major < 8)
            {
                //  Ignore manifests prior to 8.0.100, they were not side-by-side
                continue;
            }
            var manifestFeatureBand = new SdkFeatureBand(releaseVersion);

            foreach (var manifestIDFolder in Directory.GetDirectories(manifestFeatureBandFolder))
            {
                var lowerCasedManifestID = Path.GetFileName(manifestIDFolder);
                if (ManifestIdCasing.TryGetValue(lowerCasedManifestID, out string manifestID))
                {
                    foreach (var manifestVersionFolder in Directory.GetDirectories(manifestIDFolder))
                    {
                        string manifestVersionString = Path.GetFileName(manifestVersionFolder);

                        if (discoveredManifests.Contains((manifestID, manifestVersionString)))
                        {
                            continue;
                        }

                        Log.LogMessage($"Discovered manifest installation in {manifestVersionFolder} which didn't have corresponding installation record in Registry");

                        if (NuGetVersion.TryParse(manifestVersionString, out NuGetVersion manifestVersion))
                        {
                            var packageIdentity = new PackageIdentity(manifestID + ".Manifest-" + manifestFeatureBand, manifestVersion);
                            string uuidName = $"{packageIdentity};{HostArchitecture}";
                            var upgradeCode = '{' + CreateUuid(UpgradeCodeNamespaceUuid, uuidName).ToString() + '}';
                            Log.LogMessage($"Looking for upgrade code {upgradeCode} for {uuidName}");
                            List<string> relatedProductCodes;
                            try
                            {
                                relatedProductCodes = [.. WindowsInstaller.FindRelatedProducts(upgradeCode.ToString())];
                            }
                            catch (WindowsInstallerException)
                            {
                                Console.WriteLine("Error getting related products for " + upgradeCode);
                                throw;
                            }
                            DependencyProvider dependencyProvider;
                            if (relatedProductCodes.Count == 1 &&
                                DetectPackage(relatedProductCodes[0], out Version installedVersion) == DetectState.Present &&
                                (dependencyProvider = DependencyProvider.GetFromProductCode(relatedProductCodes[0])) != null)
                            {
                                var manifestRecord = new WorkloadManifestRecord();
                                manifestRecord.ProductCode = relatedProductCodes[0];
                                manifestRecord.UpgradeCode = upgradeCode;
                                manifestRecord.ManifestId = manifestID;
                                manifestRecord.ManifestVersion = manifestVersion.ToString();
                                manifestRecord.ManifestFeatureBand = manifestFeatureBand.ToString();
                                manifestRecord.ProductVersion = installedVersion;
                                manifestRecord.ProviderKeyName = dependencyProvider.ProviderKeyName;

                                manifestRecords.Add(manifestRecord);

                                Log.LogMessage($"Found installed manifest: {manifestRecord.ProviderKeyName}, {manifestRecord.ProductCode}");
                            }
                            else if (relatedProductCodes.Count > 1)
                            {
                                Log.LogMessage($"Found multiple product codes for {uuidName}, which is not expected for side-by-side manifests: {string.Join(' ', relatedProductCodes)}");
                            }
                            else
                            {
                                Log.LogMessage($"Found manifest on disk for {uuidName}, but did not find installation information in registry.");
                            }

                        }
                        else
                        {
                            Log.LogMessage($"Skipping invalid manifest version for {manifestVersionFolder}");
                        }
                    }
                }
                else
                {
                    Log.LogMessage($"Skipping unknown manifest ID {lowerCasedManifestID}.");
                }
            }
        }

        return manifestRecords;
    }

    //  From dotnet/arcade: https://github.com/dotnet/arcade/blob/c3f5cbfb2829795294f5c2d9fa5a0522f47e91fb/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Msi/MsiBase.wix.cs#L38
    /// <summary>
    /// The UUID namespace to use for generating an upgrade code.
    /// </summary>
    internal static readonly Guid UpgradeCodeNamespaceUuid = Guid.Parse("C743F81B-B3B5-4E77-9F6D-474EFF3A722C");


    //  From dotnet/arcade: https://github.com/dotnet/arcade/blob/c3f5cbfb2829795294f5c2d9fa5a0522f47e91fb/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs#L128
    /// <summary>
    /// Generates a version 3 UUID given a namespace UUID and name. This is based on the algorithm described in
    /// RFC 4122 (https://tools.ietf.org/html/rfc4122), section 4.3.
    /// </summary>
    /// <param name="namespaceUuid">The UUID representing the namespace.</param>
    /// <param name="name">The name for which to generate a UUID within the given namespace.</param>
    /// <returns>A UUID generated using the given namespace UUID and name.</returns>
    public static Guid CreateUuid(Guid namespaceUuid, string name)
    {
        // 1. Convert the name to a canonical sequence of octets (as defined by the standards or conventions of its name space); put the name space ID in network byte order. 
        byte[] namespaceBytes = namespaceUuid.ToByteArray();
        // Octet 0-3
        int timeLow = IPAddress.HostToNetworkOrder(BitConverter.ToInt32(namespaceBytes, 0));
        // Octet 4-5
        short timeMid = IPAddress.HostToNetworkOrder(BitConverter.ToInt16(namespaceBytes, 4));
        // Octet 6-7
        short timeHiVersion = IPAddress.HostToNetworkOrder(BitConverter.ToInt16(namespaceBytes, 6));

        // 2. Compute the hash of the namespace ID concatenated with the name
        byte[] nameBytes = Encoding.Unicode.GetBytes(name);
        byte[] hashBuffer = new byte[namespaceBytes.Length + nameBytes.Length];

        Buffer.BlockCopy(BitConverter.GetBytes(timeLow), 0, hashBuffer, 0, 4);
        Buffer.BlockCopy(BitConverter.GetBytes(timeMid), 0, hashBuffer, 4, 2);
        Buffer.BlockCopy(BitConverter.GetBytes(timeHiVersion), 0, hashBuffer, 6, 2);
        Buffer.BlockCopy(namespaceBytes, 8, hashBuffer, 8, 8);
        Buffer.BlockCopy(nameBytes, 0, hashBuffer, 16, nameBytes.Length);
        byte[] hash;

        using (System.Security.Cryptography.SHA256 sha256 = System.Security.Cryptography.SHA256.Create())
        {
            hash = sha256.ComputeHash(hashBuffer);
        }

        Array.Resize(ref hash, 16);

        // 3. Set octets zero through 3 of the time_low field to octets zero through 3 of the hash. 
        timeLow = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(hash, 0));
        Buffer.BlockCopy(BitConverter.GetBytes(timeLow), 0, hash, 0, 4);

        // 4. Set octets zero and one of the time_mid field to octets 4 and 5 of the hash. 
        timeMid = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(hash, 4));
        Buffer.BlockCopy(BitConverter.GetBytes(timeMid), 0, hash, 4, 2);

        // 5. Set octets zero and one of the time_hi_and_version field to octets 6 and 7 of the hash. 
        timeHiVersion = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(hash, 6));

        // 6. Set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3. 
        timeHiVersion = (short)((timeHiVersion & 0x0fff) | 0x3000);
        Buffer.BlockCopy(BitConverter.GetBytes(timeHiVersion), 0, hash, 6, 2);

        // 7. Set the clock_seq_hi_and_reserved field to octet 8 of the hash. 
        // 8. Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively.
        hash[8] = (byte)((hash[8] & 0x3f) | 0x80);

        // Steps 9-11 are essentially no-ops, but provided for completion sake
        // 9. Set the clock_seq_low field to octet 9 of the hash.
        // 10. Set octets zero through five of the node field to octets 10 through 15 of the hash.
        // 11. Convert the resulting UUID to local byte order. 

        return new Guid(hash);
    }

    /// <summary>
    /// Detect installed workload pack records. Only the default registry hive is searched. Finding a workload pack
    /// record does not necessarily guarantee that the MSI is installed.
    /// </summary>
    protected List<WorkloadPackRecord> GetWorkloadPackRecords()
    {
        Log?.LogMessage($"Detecting installed workload packs for {HostArchitecture}.");
        List<WorkloadPackRecord> workloadPackRecords = [];
        using RegistryKey installedPacksKey = Registry.LocalMachine.OpenSubKey(@$"SOFTWARE\Microsoft\dotnet\InstalledPacks\{HostArchitecture}");

        static void SetRecordMsiProperties(WorkloadPackRecord record, RegistryKey key)
        {
            record.ProviderKeyName = (string)key.GetValue("DependencyProviderKey");
            record.ProductCode = (string)key.GetValue("ProductCode");
            record.ProductVersion = new Version((string)key.GetValue("ProductVersion"));
            record.UpgradeCode = (string)key.GetValue("UpgradeCode");
        }

        if (installedPacksKey != null)
        {
            foreach (string packId in installedPacksKey.GetSubKeyNames())
            {
                using RegistryKey packKey = installedPacksKey.OpenSubKey(packId);

                foreach (string packVersion in packKey.GetSubKeyNames())
                {
                    using RegistryKey packVersionKey = packKey.OpenSubKey(packVersion);

                    WorkloadPackRecord record = new WorkloadPackRecord
                    {
                        MsiId = packId,
                        MsiNuGetVersion = packVersion,
                    };

                    SetRecordMsiProperties(record, packVersionKey);

                    record.InstalledPacks.Add((new WorkloadPackId(packId), new NuGetVersion(packVersion)));

                    Log?.LogMessage($"Found workload pack record, Id: {packId}, version: {packVersion}, ProductCode: {record.ProductCode}, provider key: {record.ProviderKeyName}");

                    workloadPackRecords.Add(record);
                }
            }
        }

        //  Workload pack group installation records are in a similar format as the pack installation records.  They use the "InstalledPackGroups" key,
        //  and under the key for each pack group/version are keys for the workload pack IDs and versions that are in the pack gorup.
        using RegistryKey installedPackGroupsKey = Registry.LocalMachine.OpenSubKey(@$"SOFTWARE\Microsoft\dotnet\InstalledPackGroups\{HostArchitecture}");
        if (installedPackGroupsKey != null)
        {
            foreach (string packGroupId in installedPackGroupsKey.GetSubKeyNames())
            {
                using RegistryKey packGroupKey = installedPackGroupsKey.OpenSubKey(packGroupId);
                foreach (string packGroupVersion in packGroupKey.GetSubKeyNames())
                {
                    using RegistryKey packGroupVersionKey = packGroupKey.OpenSubKey(packGroupVersion);

                    WorkloadPackRecord record = new WorkloadPackRecord
                    {
                        MsiId = packGroupId,
                        MsiNuGetVersion = packGroupVersion
                    };

                    SetRecordMsiProperties(record, packGroupVersionKey);

                    Log?.LogMessage($"Found workload pack group record, Id: {packGroupId}, version: {packGroupVersion}, ProductCode: {record.ProductCode}, provider key: {record.ProviderKeyName}");

                    foreach (string packId in packGroupVersionKey.GetSubKeyNames())
                    {
                        using RegistryKey packIdKey = packGroupVersionKey.OpenSubKey(packId);
                        foreach (string packVersion in packIdKey.GetSubKeyNames())
                        {
                            record.InstalledPacks.Add((new WorkloadPackId(packId), new NuGetVersion(packVersion)));
                            Log?.LogMessage($"Found workload pack in group, Id: {packId}, version: {packVersion}");
                        }
                    }

                    workloadPackRecords.Add(record);
                }
            }
        }

        return workloadPackRecords;
    }
}
